package com.gitee.wsl.time.expiry

import com.gitee.wsl.func.BiFunction
import com.gitee.wsl.time.MAX_DURATION
import com.gitee.wsl.time.MIN_DURATION
import kotlin.time.Duration

/**
 * Calculates when cache entries expire. A single expiration time is retained so that the lifetime
 * of an entry may be extended or reduced by subsequent evaluations.
 *
 *
 * Usage example:
 * {@snippet :
 * * LoadingCache<Key></Key>, Graph> cache = Caffeine.newBuilder()
 * *     .expireAfter(Expiry.creating((Key key, Graph graph) ->
 * *         Duration.between(Instant.now(), graph.createdOn().plusHours(5))))
 * *     .build(key -> createExpensiveGraph(key));
 * * }
 *
 * @param <K> the type of keys
 * @param <V> the type of values
 * @author ben.manes@gmail.com (Ben Manes)
</V></K> */
interface Expiry<K, V> {
    /**
     * Specifies that the entry should be automatically removed from the cache once the duration has
     * elapsed after the entry's creation. To indicate no expiration, an entry may be given an
     * excessively long period, such as [Long.MAX_VALUE].
     *
     *
     * **Note:** The `currentTime` is supplied by the configured [Ticker] and by
     * default does not relate to system or wall-clock time. When calculating the duration based on a
     * timestamp, the current time should be obtained independently.
     *
     * @param key the key associated with this entry
     * @param value the value associated with this entry
     * @param currentTime the ticker's current time, in nanoseconds
     * @return the length of time before the entry expires, in nanoseconds
     */
    fun expireAfterCreate(key: K, value: V, currentTime: Long): Long

    /**
     * Specifies that the entry should be automatically removed from the cache once the duration has
     * elapsed after the replacement of its value. To indicate no expiration, an entry may be given an
     * excessively long period, such as [Long.MAX_VALUE]. The `currentDuration` may be
     * returned to not modify the expiration time.
     *
     *
     * **Note:** The `currentTime` is supplied by the configured [Ticker] and by
     * default does not relate to system or wall-clock time. When calculating the duration based on a
     * timestamp, the current time should be obtained independently.
     *
     * @param key the key associated with this entry
     * @param value the new value associated with this entry
     * @param currentTime the ticker's current time, in nanoseconds
     * @param currentDuration the entry's current duration, in nanoseconds
     * @return the length of time before the entry expires, in nanoseconds
     */
    fun expireAfterUpdate(key: K, value: V, currentTime: Long, currentDuration: Long): Long

    /**
     * Specifies that the entry should be automatically removed from the cache once the duration has
     * elapsed after its last read. To indicate no expiration, an entry may be given an excessively
     * long period, such as [Long.MAX_VALUE]. The `currentDuration` may be returned to not
     * modify the expiration time.
     *
     *
     * **Note:** The `currentTime` is supplied by the configured [Ticker] and by
     * default does not relate to system or wall-clock time. When calculating the duration based on a
     * timestamp, the current time should be obtained independently.
     *
     * @param key the key associated with this entry
     * @param value the value associated with this entry
     * @param currentTime the ticker's current time, in nanoseconds
     * @param currentDuration the entry's current duration, in nanoseconds
     * @return the length of time before the entry expires, in nanoseconds
     */
    fun expireAfterRead(key: K, value: V, currentTime: Long, currentDuration: Long): Long

    companion object {
        /**
         * Returns an `Expiry` that specifies that the entry should be automatically removed from
         * the cache once the duration has elapsed after the entry's creation. The expiration time is
         * not modified when the entry is updated or read.
         *
         * {@snippet :
         * * Expiry<Key></Key>, Graph> expiry = Expiry.creating((key, graph) ->
         * *     Duration.between(Instant.now(), graph.createdOn().plusHours(5)));
         * * }
         *
         * @param <K> the key type
         * @param <V> the value type
         * @param function the function used to calculate the length of time after an entry is created
         * before it should be automatically removed
         * @return an `Expiry` instance with the specified expiry function
        </V></K> */
        fun <K, V> creating(function: BiFunction<K, V, Duration>): Expiry<K, V> {
            return ExpiryAfterCreate(function)
        }

        /**
         * Returns an `Expiry` that specifies that the entry should be automatically removed from
         * the cache once the duration has elapsed after the entry's creation or replacement of its value.
         * The expiration time is not modified when the entry is read.
         *
         * {@snippet :
         * * Expiry<Key></Key>, Graph> expiry = Expiry.writing((key, graph) ->
         * *     Duration.between(Instant.now(), graph.modifiedOn().plusHours(5)));
         * * }
         *
         * @param <K> the key type
         * @param <V> the value type
         * @param function the function used to calculate the length of time after an entry is created
         * or updated that it should be automatically removed
         * @return an `Expiry` instance with the specified expiry function
        </V></K> */
        fun <K, V> writing(function: BiFunction<K, V, Duration>): Expiry<K, V> {
            return ExpiryAfterWrite(function)
        }

        /**
         * Returns an `Expiry` that specifies that the entry should be automatically removed from
         * the cache once the duration has elapsed after the entry's creation, replacement of its value,
         * or after it was last read.
         *
         * {@snippet :
         * * Expiry<Key></Key>, Graph> expiry = Expiry.accessing((key, graph) ->
         * *     graph.isDirected() ? Duration.ofHours(1) : Duration.ofHours(3));
         * * }
         *
         * @param <K> the key type
         * @param <V> the value type
         * @param function the function used to calculate the length of time after an entry last accessed
         * that it should be automatically removed
         * @return an `Expiry` instance with the specified expiry function
        </V></K> */
        fun <K, V> accessing(function: BiFunction<K, V, Duration>): Expiry<K, V> {
            return ExpiryAfterAccess(function)
        }
    }
}

internal class ExpiryAfterCreate<K, V>(
    val function: BiFunction<K, V, Duration>,
) : Expiry<K, V>{

    override fun expireAfterCreate(key: K, value: V, currentTime: Long): Long {
        return toNanosSaturated(function.apply(key, value))
    }

    override fun expireAfterUpdate(
        key: K,
        value: V,
        currentTime: Long,
        currentDuration: Long,
    ): Long {
        return currentDuration
    }

    override fun expireAfterRead(key: K, value: V, currentTime: Long, currentDuration: Long): Long {
        return currentDuration
    }

}

internal class ExpiryAfterWrite<K, V>(
    val function: BiFunction<K, V, Duration>,
) : Expiry<K, V>{

    override fun expireAfterCreate(key: K, value: V, currentTime: Long): Long {
        return toNanosSaturated(function.apply(key, value))
    }

    override fun expireAfterUpdate(
        key: K,
        value: V,
        currentTime: Long,
        currentDuration: Long,
    ): Long {
        return toNanosSaturated(function.apply(key, value))
    }


    override fun expireAfterRead(key: K, value: V, currentTime: Long, currentDuration: Long): Long {
        return currentDuration
    }

}

internal class ExpiryAfterAccess<K, V>(
    val function: BiFunction<K, V, Duration>,
) : Expiry<K, V>{


    override fun expireAfterCreate(key: K, value: V, currentTime: Long): Long {
        return toNanosSaturated(function.apply(key, value))
    }

    override fun expireAfterUpdate(
        key: K,
        value: V,
        currentTime: Long,
        currentDuration: Long,
    ): Long {
        return toNanosSaturated(function.apply(key, value))
    }

    override fun expireAfterRead(key: K, value: V, currentTime: Long, currentDuration: Long): Long {
        return toNanosSaturated(function.apply(key, value))
    }

}

private fun toNanosSaturated(duration: Duration): Long {
    return if (duration.isNegative())
        if (duration <= Duration.MIN_DURATION) Long.MIN_VALUE else duration.inWholeNanoseconds
    else
        if (duration >= Duration.MAX_DURATION) Long.MAX_VALUE else duration.inWholeNanoseconds
}